perf(investments): cut InvestmentsPage DB fanout and add CI#2
Merged
Conversation
Adds a `node:24-alpine` Dockerfile that bakes in deps and sources and defaults to `pnpm start` (prod). The compose file overrides `command` to `pnpm dev` and volume-mounts the host source tree for hot reload, and points the container at the in-network Jaeger for OTLP traces. Also pins Node to 24 via `.tool-versions` + `engines` so host and container match.
Wraps `loadInvestmentStats` in `contextAwareDataLoader` keyed on `(investmentId, assetId)`, and threads `Context` through the four callers that hit it. Cuts the per-investment query fanout in half (46 → 23 per `InvestmentsPage`), since the `investments()` sort and `Investment.position` resolver no longer re-run the four underlying selects for the same investment.
Replaces the per-id memo with a microtask-coalesced batcher: every `loadInvestmentStats(ctx, id)` call issued within the same tick is folded into four `IN (...)` selects — one each for `Investments`, `InvestmentTransactions`, `InvestmentStockSplits`, and `InvestmentPrices`. Stats are then computed from the grouped rows. On `InvestmentsPage`, the four per-id queries that used to fire 23× each collapse into a single batched round-trip each, dropping the total drizzle-span count from ~230 to ~50 and halving cumulative DB time. `assetId` filtering runs in JS over the batched transactions so per-wrapper resolvers share the same fetch.
Every Portfolio field resolver (totalValue, totalCost, totalGain, percentGain, xirr, dailyGainValue, dailyGainPercent, timeseries, candlestick) used to call `loadHeldInvestments` independently, firing the same four `IN (...)` selects once per field. `Portfolio` now has a private `loadHeld(opts)` method that caches by normalised `skipLive` flag, and `computePortfolioXirr` / `loadDailySeriesMinor` accept the already-loaded held list as a parameter so they share the cache. On the `InvestmentsPage` query this drops drizzle spans from ~29 to ~17 and cumulative DB time from ~450 ms to ~100 ms.
Two parallel jobs: - `lint`: runs `pnpm typecheck` and `pnpm exec eslint .` at the repo root. - `backend-test`: spins up Postgres 18 on port 5433 (matching `test/global-setup.ts`) and runs `pnpm --filter backend test`.
- `pnpm exec eslint . --fix` to apply prettier formatting across the tree. - Allow `process` / `console` as globals in `**/*.mjs` (covers `packages/backend/otel.mjs`, which runs as a plain Node script). - Drop the unused `FormSection` helper and its orphaned imports from `packages/web/src/components/net-worth/entry-form.tsx`. - Rename unused `year` destructures to `_year` in `packages/web/src/routes/planning/$year.tsx`. `pnpm exec eslint .` is now clean.
felamaslen
added a commit
that referenced
this pull request
Apr 27, 2026
perf(investments): cut InvestmentsPage DB fanout and add CI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
InvestmentsPagequery's DB fanout from ~230 drizzle spans → ~17, and cumulative DB time from ~8.9 s → ~100 ms, by batchingloadInvestmentStatswithdataloaderand memoisingloadHeldInvestmentsperPortfolioinstance.packages/backend/Dockerfile, compose override), adds a root.dockerignore, and pins Node 24 via.tool-versions..github/workflows/ci.yml— a singletestjob that runspnpm typecheck,pnpm exec eslint ., thenpnpm --filter backend testagainst a Postgres 18 service.Perf —
InvestmentsPage(sampled via Jaeger, 3 traces each)loadInvestmentStatsdataloaderbatch (oneIN (...)per table)Portfoliomemo ofloadHeldInvestmentsInfra
Dockerfileis prod-oriented (defaultCMDrunspnpm start); compose overrides topnpm devand bind-mounts the host source tree sovite-nodehot-reloads without rebuilds.OTEL_*env points the container at the in-network Jaeger so traces show up in the existingjaegertracing/all-in-oneservice.Test plan
InvestmentsPagerenders identical totals / gains in the browser.docker compose up -dbrings backend, Postgres, and Jaeger up cleanly; traces appear at http://localhost:16686 underfire-backend.